在原本 Laravel Blade 裡要使用分頁是非常簡單的,但現在我們前端是 Vue,需要做一(億)些些調整才能使用分頁功能。
只要把 get()
改成 paginate()
就可以使用 Laravel 預設的分頁器功能:
app/Http/Controllers/Post/PostController.php
public function index()
{
$posts = $this->user()
->posts()
->where('published', true)
->latest()
->paginate();
return Inertia::render('Post/List', [
'type' => 'published',
'typeText' => '文章',
'posts' => PostPresenter::collection($posts)
->preset('list')
->get(),
]);
}
public function drafts()
{
$posts = $this->user()
->posts()
->where('published', false)
->latest()
->paginate();
return Inertia::render('Post/List', [
'type' => 'drafts',
'typeText' => '草稿',
'posts' => PostPresenter::collection($posts)
->preset('list')
->get(),
]);
}
設定 Post Model 分頁都是一頁顯示 10 筆資料:
app/Post.php
protected $perPage = 10;
但預設的分頁器輸出的資料不完全是我們要的,現在要新增一個繼承自 LengthAwarePaginator
的分頁器,然後稍微調整一下。首先要改 url()
,增加判斷當前輸出連結是不是第一頁,如果是就會把後面的 ?page=1
自動減掉;linkCollection()
會輸出使適用於 Inertia 的分頁資料;最後在 toArray()
裡指定需要輸出的完整分頁器的資料,多一個 showPaginator
決定前端需不需要渲染分頁組件:
Laravel 8 以上
linkCollection()
方法預設已經存在,不需要自訂。
app/Pagination/Paginator.php
<?php
namespace App\Pagination;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class Paginator extends LengthAwarePaginator
{
public function url($page)
{
if ($page <= 0) {
$page = 1;
}
$parameters = $page === 1 ? [] : [$this->pageName => $page];
if (count($this->query) > 0) {
$parameters = array_merge($this->query, $parameters);
}
$divide = Str::contains($this->path(), '?') ? '&' : '?';
$queryString = count($parameters)
? ($divide.Arr::query($parameters))
: '';
return $this->path().$queryString.$this->buildFragment();
}
public function linkCollection()
{
return collect($this->elements())
->flatMap(fn ($item) => is_array($item)
? collect($item)->map(fn ($url, $page) => [
'url' => $url,
'label' => $page,
'active' => $this->currentPage() === $page,
])
: [['url' => null, 'label' => '...', 'active' => false]]
)
->prepend([
'url' => $this->previousPageUrl(),
'label' => __('pagination.previous'),
'active' => false,
])
->push([
'url' => $this->nextPageUrl(),
'label' => __('pagination.next'),
'active' => false,
]);
}
public function toArray()
{
return [
'data' => $this->items->toArray(),
'links' => $this->linkCollection()->toArray(),
'showPaginator' => $this->hasPages(),
];
}
}
然後綁定新分頁器,變成以後只要在 Container 裡讀到 LengthAwarePaginator
,就會自動替換成我們定義的新分頁器:
app/Providers/AppServiceProvider.php
use App\Pagination\Paginator;
use Illuminate\Pagination\LengthAwarePaginator;
public function register()
{
$this->registerInertia();
$this->registerLengthAwarePaginator();
}
protected function registerLengthAwarePaginator()
{
$this->app->bind(LengthAwarePaginator::class, Paginator::class);
}
再來做分頁組件,根據在上面設定的 linkCollection()
分頁連結資料。這裡有做電腦版和手機版,電腦版有完整的分頁,手機版只會顯示上/下一頁兩個按鈕:
resources/js/Components/Pagination.vue
<template>
<div class="font-medium">
<div class="flex justify-between md:hidden">
<template v-for="(link, key) in simpleLinks">
<div v-if="link.url === null" :key="link.label" class="pagination-item pagination-disabled">{{ link.label }}</div>
<inertia-link v-else :key="link.label" class="pagination-item pagination-link" :href="link.url">{{ link.label }}</inertia-link>
</template>
</div>
<div class="hidden md:flex md:flex-wrap">
<template v-for="link in links">
<div v-if="link.active" :key="link.label" class="pagination-item pagination-active mr-2 mt-2">{{ link.label }}</div>
<div v-else-if="link.url === null" :key="link.label" class="pagination-item pagination-disabled mr-2 mt-2">{{ link.label }}</div>
<inertia-link v-else :key="link.label" class="pagination-item pagination-link mr-2 mt-2" :href="link.url">{{ link.label }}</inertia-link>
</template>
</div>
</div>
</template>
<script>
export default {
props: {
links: Array
},
computed: {
simpleLinks() {
return [
this.links.slice(0).shift(),
this.links.slice(-1).pop()
]
}
}
}
</script>
和分頁組件的樣式:
resources/css/pagination.css
.pagination-item {
@apply px-3 py-1 text-sm font-light border rounded-md select-none;
}
.pagination-link {
@apply text-gray-500 border-gray-300 transition-colors duration-150;
&:hover {
@apply border-purple-500 text-purple-500;
}
}
.pagination-active {
@apply bg-purple-500 border-purple-500 text-white;
}
.pagination-disabled {
@apply text-gray-300 border-gray-200;
}
resources/css/app.css
@import 'tailwindcss/components';
...
@import 'pagination';
...
有了分頁組件,現在就可以加到文章列表組件裡。記得上面有調整了分頁集合渲染出來的資料?現在的文章集合是 Object
不是 Array
,還有真正的文章們現在在 posts.data
,這些都要調整:
resources/js/Lightning/PostList.vue
<template>
<div>
<ul v-if="posts.data.length" class="divide-y -my-6">
<li v-for="post in posts.data" class="py-6">
...
</li>
</ul>
...
<pagination v-if="posts.showPaginator" class="mt-8" :links="posts.links" />
</div>
</template>
<script>
import Pagination from '@/Components/Pagination'
export default {
components: {
Pagination
},
props: {
posts: {
type: Object,
required: true
},
...
}
}
</script>
列表頁面也不要忘記改 posts
的型別:
resources/js/Pages/Post/List.vue
<script>
export default {
props: {
posts: Object
}
}
</script>
還要改一下翻譯文字:
resources/lang/zh_TW/pagination.php
return [
'previous' => '上一頁',
'next' => '下一頁',
];
補上我的文章的選單連結:
resources/js/Layouts/AppLayout.vue
<template #menu="{ close }">
<dropdown-item :href="`/user/${user.id}`" icon="heroicons-outline:home" @click="close">
我的主頁
</dropdown-item>
... <!-- 撰寫文章 -->
<dropdown-item href="/posts" icon="heroicons-outline:book-open" @click="close">
我的文章
</dropdown-item>
<dropdown-item href="/posts/drafts" icon="heroicons-outline:document-text" @click="close">
我的草稿
</dropdown-item>
...
</template>
最後新增一堆假文章 (看不懂正常,這些都是假文產生器產生的...):
>>> App\User::find(1)->posts()->saveMany(factory(App\Post::class, 60)->make())
分頁功能完成~~
這分頁功能雖然需要做點小調整,但整體來說不算太難。前端也依然是熟悉的 Vue 組件。到此為止列表也大概完成了,下篇就可以在用戶的個人頁面加入文章列表了。看起來很簡單,但似乎隱藏著危機?
Lightning 範例程式碼:https://github.com/ycs77/lightning
您好,关于app/Pagination/Paginator.php
中的namespace有点问题,应该为:
<?php
namespace App\Pagination;
...
非常感谢您出的很棒的教程!
謝謝糾錯XD
也謝謝您對本系列的支持!